Поляков Евгений, 2023

Прогнозирование влажности листьев


Нучно-исследовательская работа

Цель работы: Проверить предположение о возможности разработки модели прогнозирования на основе представленных данных, используя методы анализа данных и машинного обучения.


Постановка задачи¶

Влажность листьев - это метеоролигический параметр, который описывает количество росы и осадков, оставшихся на поверхности. Он используется для мониторинга влажности листьев в сельскохозяйственных целях,таких как борьба с грибками и болезнями, для контроля ирригационных систем, а также для обнаружения тумана и условий росы, а также для раннего обнаружения дождя.

Для измерения влажности листьев существуют датчики, например от австрийской компании Pessl Instruments GmbH (https://metos.at/ru/portfolio/leaf-wetness/), изображенный на рисунке 1.

Рисунок 1. Датчик PESSL INSTRUMENTS LEAF WETNESS от Компании Pessl Instruments GmbH

Для задачи анализа данных и машинного обучения представлен набор структурированных данных в виде .excel-файла содержащий измерения данного датчика. Кроме того в файле содержаться результаты прочих метерологических измерений, которые собирались синхронно, с фиксацией влажности листьев в том же месте, где располагался датчик.

В процессе проведения исследования необходимо ответить на следующие вопросы:

  1. Существует ли возможность определить влажность листьев без использования датчика.
  • Если существует возможность определения влажности по иным данным, необходимо разработать прототип модели, которая "предсказывает" влажность листьев по косвенным данным (температура почвы, влажность воздуха и др.)
  • Если такой возможность нет, необходимо обосновать данное обстоятельство и показать на примерах.
  1. Какую поезную информацию (для агроиндустрии) можно получить из представленных данных)

Результат анализа данных и моделирования представить в виде .ipynb-файла, с подробным описанием процесса.

Настройка среды¶

Импорт встроенных пакетов¶

In [1]:
import math
import os
import re
from datetime import date, datetime
from typing import Any

from IPython.display import HTML, IFrame, YouTubeVideo, clear_output, display

Настройка переменных среды¶

In [2]:
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"

Установка необходимых пакетов¶

In [3]:
!pip install pandas==1.5.3
!pip install matplotlib==3.6.3
!pip install openpyxl==3.1.0
!pip install scikit-learn==1.2.1
!pip install july==0.1.3
!pip install plotly=5.13.0
!pip install altair==4.2.2
!pip install seaborn==0.12.2
!pip install tensorflow-cpu==2.11.0
!pip install xgboost==1.7.3
!pip install ipywidgets==8.0.4
!pip install optuna==3.1.0

clear_output()

Импорт сторонних пакетов¶

In [360]:
import warnings

import altair as alt
import july
import matplotlib.pyplot as plt
import numpy as np
import optuna
import pandas as pd
import plotly
import plotly.express as px
import plotly.io as pio
import seaborn as sns
import tensorflow as tf
import xgboost
from july.utils import date_range
from keras import layers
from scipy.stats import shapiro
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.feature_selection import VarianceThreshold
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import Normalizer, StandardScaler
from sklearn.svm import LinearSVC, LinearSVR
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from tensorflow import keras

Настройка пакетов¶

In [358]:
pd.set_option("display.max_columns", 10)
alt.renderers.enable("jupyterlab")
warnings.filterwarnings("ignore")

Настройка путей¶

In [5]:
# Исходный набор данных
RAW_DATA_PATH = os.path.join("../data/station_data.xlsx")

Настройка констант¶

In [65]:
RANDOM_STATE = 2023

Разведочный анализ данных¶

Для возможности создания наиболее качественных в будущем прогнозных моделей, необходимо провести максимально подробный анализ представленных данных:

  • Определить зависимости в данных
  • Выявить недостатки данных
  • Разработать гипотезы и предположения о наличии возможности построить прогнозную модель по имеющимся исходным данным

Обзор исходных данных¶

Загрузка данных¶

Загрузка исходного набора данных для исследования

In [6]:
raw_data = pd.read_excel(RAW_DATA_PATH)
raw_data.head(n=3)
Out[6]:
Unnamed: 0 Температура воздуха [°C] Unnamed: 2 Unnamed: 3 Точка росы [°C] ... Unnamed: 19 Солнечная панель [mV] АКБ [mV] АКБ2 [mV] Unnamed: 23
0 Дата / время ср.знач максимум минимум ср.знач ... минимум последний последний последний Эталонная эвапотранспирация ET0 [mm]
1 2020-08-31 19:00:00 19.74 19.82 19.68 16.7 ... 18 9058 6784 3643 NaN
2 2020-08-31 18:00:00 20.18 20.6 19.76 17.5 ... 17.2 9298 6793 3643 NaN

3 rows × 24 columns

Заголовок таблицы состоит из двух строк, т.е. составной. Каждый из основных столбцов состоит из нескольких столбцов расширяющих информацию о них.

Описание признаков¶

  • Дата/время - Описывает дату и время проведения замера

  • Температура воздуха [°C] - Температура воздуха во время проведения замера со средним, минимальным и максимальным значением, выражена в градусах Цельсия (https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BC%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D1%83%D1%80%D0%B0_%D0%B2%D0%BE%D0%B7%D0%B4%D1%83%D1%85%D0%B0)

  • Точка росы [°C] - Tемпература воздуха, при которой содержащийся в нём пар достигает состояния насыщения и начинает конденсироваться в росу. Выражена в градусах Цельсия и представлен средним и минимальным значением (https://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D1%87%D0%BA%D0%B0_%D1%80%D0%BE%D1%81%D1%8B)

  • Солнечная радиация [W/m2] - Электромагнитное и корпускулярное излучение Солнца. Данный параметр не означает радиацию в "бытовом" смыcле от слова ионизирующее излучение. Измеряется мощностью переносимой ею энергии на единицу площади поверхности Ватт/м^2. Представлено средним значением (https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D0%BB%D0%BD%D0%B5%D1%87%D0%BD%D0%B0%D1%8F_%D1%80%D0%B0%D0%B4%D0%B8%D0%B0%D1%86%D0%B8%D1%8F)

  • VPD [kPa] - Дефицит давления пара (https://growstuff.ru/blog/poleznaya-informatsiya/rasskazhem-pro-vpd-i-uroven-transpiratsii/), измеряется в килопаскалях (кПа) и выражено средним и минимальным значением

  • Влажность воздуха [%] - Выражена средним, максимальным и минимальным значениями (https://hvac-school.ru/upload/files/folder_28/tree_inrost.htm). Измеряется в процентах

  • Осадки [mm] - Количество осадков, измеряется в миллиметарх. (https://www.gismeteo.ru/news/klimat/29223-pochemu-osadki-izmeryayut-v-millimetrah)

  • Влажность листа [time] - Показания датчика влажности листа. Определяет факт и длительность нахождения листа во влажном состоянии.

  • Скорость ветра [m/s] - Средняя и максимальная скорость ветра в метрах в секунду.

  • Влажность почвы [%] - Среднее значение влажности почвы, в процентах

  • Температура почвы [°C] - Среднее, максимальное и минимальное значение температуры почвы в градусах Цельсия

Проблема имен столбцов¶

При певоначальном анализе представленного набора данных было выявлено несколько проблем:

  1. Все имена столбцов столбцов разбиты на подстолбцы, что не очень приемлемо для дальнейшего анализа используя библиотеку Pandas. Причем в Pandas, первая строка в наборе даннных является названиями подстолбцов, а в данном месте в названии столбца используется автоматическое переименование Unnamed ....
  2. Последний и первый столбцы которые называются Unnamed: 0 и Unnamed: 23 не имеют названий, названия имеют только их подстолбцы. Данное обстоятельство видно есть открыть файл в excel-редакторе (Рисунок 2).
  3. Представленные названия столбцов являются несколько неудобными для дальнейшего анализа (в большей степени из-за наличия пробелов и использования русского языка).
Рисунок 2. Отсутствие названий в первом и последнем столбце в исходных данных

Для упрощения работы над такими данными предполагается сделать некоторые преобразования (переименования) чтобы привести их к более распространненному виду. Наиболее приемлемый вариант - использование английских слов (перевод).

Замена имен столбцов английскими аналогами¶

Объединение названий столбцов с подстолбцами¶

Для начала необходимо избавиться от составных столбцов. Для этого сначала заменим первый и последний столбец именами подстолбцов. Далее в цикле пройдем по столбцам и объединим имена с подстолбцамм по правилу - имя столбца_имя подстолбца

In [7]:
original_rows = raw_data.columns
print("Названия оригинальных столбцов: \n\n", original_rows, "\n")
original_subrows = raw_data.iloc[0].values
print("Названия оригинальных подстолбцов: \n\n", original_subrows, "\n")
Названия оригинальных столбцов: 

 Index(['Unnamed: 0', 'Температура воздуха [°C]', 'Unnamed: 2', 'Unnamed: 3',
       'Точка росы [°C]', 'Unnamed: 5', 'Солнечная радиация [W/m2]',
       'VPD [kPa]', 'Unnamed: 8', 'Влажность воздуха  [%]', 'Unnamed: 10',
       'Unnamed: 11', 'Осадки [mm]', 'Влажность листа [минимум]',
       'Скорость ветра [m/s]', 'Unnamed: 15', 'Влажность почвы [%]',
       'Температура почвы [°C]', 'Unnamed: 18', 'Unnamed: 19',
       'Солнечная панель [mV]', 'АКБ [mV]', 'АКБ2 [mV]', 'Unnamed: 23'],
      dtype='object') 

Названия оригинальных подстолбцов: 

 ['Дата / время' 'ср.знач' 'максимум' 'минимум' 'ср.знач' 'минимум'
 'ср.знач' 'ср.знач' 'минимум' 'ср.знач' 'максимум' 'минимум' 'сумма'
 'time' 'ср.знач' 'максимум' 'ср.знач' 'ср.знач' 'максимум' 'минимум'
 'последний' 'последний' 'последний'
 'Эталонная эвапотранспирация ET0 [mm]'] 

In [8]:
# Измененные имена столбцов
changed_column_names = []
for i, (col, sub_col) in enumerate(zip(original_rows, original_subrows)):
    if not str(col).strip().startswith("Unnamed"):
        real_col_name = col
    if i == 0 or i == (len(original_rows) - 1):
        changed_column_names.append(sub_col)
    else:
        changed_column_names.append(f"{real_col_name}_{sub_col}")

print("Измененнные имена столбцов: \n\n", changed_column_names, "\n")
Измененнные имена столбцов: 

 ['Дата / время', 'Температура воздуха [°C]_ср.знач', 'Температура воздуха [°C]_максимум', 'Температура воздуха [°C]_минимум', 'Точка росы [°C]_ср.знач', 'Точка росы [°C]_минимум', 'Солнечная радиация [W/m2]_ср.знач', 'VPD [kPa]_ср.знач', 'VPD [kPa]_минимум', 'Влажность воздуха  [%]_ср.знач', 'Влажность воздуха  [%]_максимум', 'Влажность воздуха  [%]_минимум', 'Осадки [mm]_сумма', 'Влажность листа [минимум]_time', 'Скорость ветра [m/s]_ср.знач', 'Скорость ветра [m/s]_максимум', 'Влажность почвы [%]_ср.знач', 'Температура почвы [°C]_ср.знач', 'Температура почвы [°C]_максимум', 'Температура почвы [°C]_минимум', 'Солнечная панель [mV]_последний', 'АКБ [mV]_последний', 'АКБ2 [mV]_последний', 'Эталонная эвапотранспирация ET0 [mm]'] 

Замена названий столбцов в представленном наборе данных¶

Теперь неообходимо заменить исходыне имена в наборе данных, на новые имена, и удалить строку с названиями подстолбцов которая находилась в исходном наборе данных.

In [9]:
# Создание карты исходынх и новых имен столбцов
renamaining_map = {
    original_cols: changed_cols
    for original_cols, changed_cols in zip(
        original_rows, changed_column_names
    )
}
# Переименование имен столбцов  и копирование в новую переменную
header_changed_data = raw_data.rename(columns=renamaining_map).copy()
# Удаление строки содержащей имена подстолбцов
header_changed_data = header_changed_data.iloc[1:]
header_changed_data.head(3)
Out[9]:
Дата / время Температура воздуха [°C]_ср.знач Температура воздуха [°C]_максимум Температура воздуха [°C]_минимум Точка росы [°C]_ср.знач ... Температура почвы [°C]_минимум Солнечная панель [mV]_последний АКБ [mV]_последний АКБ2 [mV]_последний Эталонная эвапотранспирация ET0 [mm]
1 2020-08-31 19:00:00 19.74 19.82 19.68 16.7 ... 18 9058 6784 3643 NaN
2 2020-08-31 18:00:00 20.18 20.6 19.76 17.5 ... 17.2 9298 6793 3643 NaN
3 2020-08-31 17:00:00 20.57 21.09 20.33 17.3 ... 17.2 8349 6787 3643 NaN

3 rows × 24 columns

Переименование столбцов (первод на аншлийский язык)¶

На данном этапе нобходимо создать маску для переименования исходных столбцов в новые имена на английском языке, и переименовать столбцы.

In [10]:
translated_columns_map = {
    "Дата / время": "datetime",
    "Температура воздуха [°C]_ср.знач": "air_temperature_mean",
    "Температура воздуха [°C]_максимум": "air_temperature_max",
    "Температура воздуха [°C]_минимум": "air_temperature_min",
    "Точка росы [°C]_ср.знач": "dew_point_mean",
    "Точка росы [°C]_минимум": "dew_point_min",
    "Солнечная радиация [W/m2]_ср.знач": "solar_radiation_mean",
    "VPD [kPa]_ср.знач": "vpd_mean",
    "VPD [kPa]_минимум": "vpd_min",
    "Влажность воздуха  [%]_ср.знач": "air_humidity_mean",
    "Влажность воздуха  [%]_максимум": "air_humidity_max",
    "Влажность воздуха  [%]_минимум": "air_humidity_min",
    "Осадки [mm]_сумма": "precipitation",
    "Влажность листа [минимум]_time": "leaf_wetness",
    "Скорость ветра [m/s]_ср.знач": "wind_speed_mean",
    "Скорость ветра [m/s]_максимум": "wind_speed_max",
    "Влажность почвы [%]_ср.знач": "soil_wetness_mean",
    "Температура почвы [°C]_ср.знач": "soil_temperature_mean",
    "Температура почвы [°C]_максимум": "soil_temperature_max",
    "Температура почвы [°C]_минимум": "soil_temperature_min",
    "Солнечная панель [mV]_последний": "solar_panel",
    "АКБ [mV]_последний": "battery1",
    "АКБ2 [mV]_последний": "battery2",
    "Эталонная эвапотранспирация ET0 [mm]": "eto",
}
translated_header_data = header_changed_data.rename(
    columns=translated_columns_map
)
translated_header_data.head(3)
Out[10]:
datetime air_temperature_mean air_temperature_max air_temperature_min dew_point_mean ... soil_temperature_min solar_panel battery1 battery2 eto
1 2020-08-31 19:00:00 19.74 19.82 19.68 16.7 ... 18 9058 6784 3643 NaN
2 2020-08-31 18:00:00 20.18 20.6 19.76 17.5 ... 17.2 9298 6793 3643 NaN
3 2020-08-31 17:00:00 20.57 21.09 20.33 17.3 ... 17.2 8349 6787 3643 NaN

3 rows × 24 columns

Выводы по обзору исходных данных¶

  1. Набор данных представляет собой excel-файл c таблицей разннообразных изменений
  2. Таблица данных содержит столбцы которые разделены на подстолбцы. Итоговое количество столбцов - 24
  3. Имена столбцов имеют неудобный вид. Необходимо преобразовать заголовок таблицы используя имена на английском языке

Первичный анализ данных¶

Отсуствующие данные¶

Отсуствующие данные - важный параметр, часто помогает улучшить модель, если правильно обработать отсуствие данных.

In [11]:
(
    translated_header_data.isna().sum() / len(translated_header_data) * 100
).apply(math.floor)
Out[11]:
datetime                  0
air_temperature_mean      0
air_temperature_max       0
air_temperature_min       0
dew_point_mean            0
dew_point_min             0
solar_radiation_mean      0
vpd_mean                  0
vpd_min                   0
air_humidity_mean         0
air_humidity_max          0
air_humidity_min          0
precipitation             0
leaf_wetness              0
wind_speed_mean           0
wind_speed_max            0
soil_wetness_mean         0
soil_temperature_mean     0
soil_temperature_max      0
soil_temperature_min      0
solar_panel               0
battery1                  0
battery2                  0
eto                      95
dtype: int64

Только столбец eto, который является Эталонной эвапотранспирацией имеет отсутствующие значения, в количестве 95%. Характер отсуствия данных является важной характеристикой, для принятия решения о действиях над данными. В следующем разделе будет исследован характер отсуствия большого количества данных в данном столбце и сделаны соответствущие выводы.

Типы данных¶

Необходимо посмотреть какие типы данных представлены в наборе

In [12]:
translated_header_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3649 entries, 1 to 3649
Data columns (total 24 columns):
 #   Column                 Non-Null Count  Dtype 
---  ------                 --------------  ----- 
 0   datetime               3649 non-null   object
 1   air_temperature_mean   3649 non-null   object
 2   air_temperature_max    3649 non-null   object
 3   air_temperature_min    3649 non-null   object
 4   dew_point_mean         3649 non-null   object
 5   dew_point_min          3649 non-null   object
 6   solar_radiation_mean   3649 non-null   object
 7   vpd_mean               3649 non-null   object
 8   vpd_min                3649 non-null   object
 9   air_humidity_mean      3649 non-null   object
 10  air_humidity_max       3649 non-null   object
 11  air_humidity_min       3649 non-null   object
 12  precipitation          3649 non-null   object
 13  leaf_wetness           3649 non-null   object
 14  wind_speed_mean        3649 non-null   object
 15  wind_speed_max         3649 non-null   object
 16  soil_wetness_mean      3649 non-null   object
 17  soil_temperature_mean  3649 non-null   object
 18  soil_temperature_max   3649 non-null   object
 19  soil_temperature_min   3649 non-null   object
 20  solar_panel            3649 non-null   object
 21  battery1               3649 non-null   object
 22  battery2               3649 non-null   object
 23  eto                    152 non-null    object
dtypes: object(24)
memory usage: 684.3+ KB

Все столбцы имеют тип object, что скорее всего свидетельствует о том, что тип данных не удалось определить автоматически, и необходимо вручную идентифицировать типы данных, для корректной работы над дальнейшим анализом.

Приведение типов данных¶

Визуально оценим тип данных в каждом из столбцов и приведем к правильному типу, исключая столбец eto, так как в нем имеются None - значения.

In [13]:
# Транспонирование таблицы данных, для более удобного анализа
translated_header_data.sample(7, random_state=3).T
Out[13]:
534 1772 693 2285 2870 3459 2053
datetime 2020-08-09 14:00:00 2020-06-19 00:00:00 2020-08-02 23:00:00 2020-05-28 15:00:00 2020-05-04 06:00:00 2020-04-09 17:00:00 2020-06-07 07:00:00
air_temperature_mean 25.92 13.39 22.41 21.64 4.52 5.29 14.69
air_temperature_max 26.94 14.13 23.88 22.64 4.68 5.74 15.17
air_temperature_min 24.74 12.9 21.57 20.29 3.97 4.81 12.79
dew_point_mean 20.8 13.3 20.2 10.7 4.4 -1.5 12.6
dew_point_min 20 12.8 19.2 9.2 3.9 -1.9 12.2
solar_radiation_mean 360 0 0 860 0 503 5
vpd_mean 0.87 0 0.33 1.28 0 0.34 0.2
vpd_min 0.64 0 0.17 1.05 0 0.32 0
air_humidity_mean 73.79 99.85 87.86 50.32 99.97 61.44 87.89
air_humidity_max 79.37 100 93.44 56.45 99.97 63.76 99.97
air_humidity_min 68.64 98.59 75.34 44.45 99.97 59.84 83.39
precipitation 0 0 0.2 0 3.4 0 0
leaf_wetness 0 0 0 0 60 0 0
wind_speed_mean 0.7 0.1 0.1 1.5 2.8 2 0.8
wind_speed_max 1.2 0.5 0.7 2.3 3.6 2.7 1.8
soil_wetness_mean 30.09 40.85 29.57 40.01 41.75 37.87 39.15
soil_temperature_mean 18.4 14.2 21.4 12.1 8.5 0.1 14.4
soil_temperature_max 19.4 17.2 23.1 12.7 9 2 15.7
soil_temperature_min 18.1 12.6 20.6 9.7 7.8 -0.5 12.1
solar_panel 9102 0 0 10051 0 10309 6408
battery1 6773 6502 6479 6790 6411 6824 6496
battery2 3634 3663 3643 3677 3696 3716 3668
eto NaN 3.2 NaN NaN NaN NaN NaN
In [14]:
data_types_map = {
    "datetime": "datetime64[ns]",
    "air_temperature_mean": "float32",
    "air_temperature_max": "float32",
    "air_temperature_min": "float32",
    "dew_point_mean": "float32",
    "dew_point_min": "float32",
    "solar_radiation_mean": "int32",
    "vpd_mean": "float32",
    "vpd_min": "float32",
    "air_humidity_mean": "float32",
    "air_humidity_max": "float32",
    "air_humidity_min": "float32",
    "precipitation": "float32",
    "leaf_wetness": "int32",
    "wind_speed_mean": "float32",
    "wind_speed_max": "float32",
    "soil_wetness_mean": "float32",
    "soil_temperature_mean": "float32",
    "soil_temperature_max": "float32",
    "soil_temperature_min": "float32",
    "solar_panel": "int32",
    "battery1": "int32",
    "battery2": "int32",
}
typed_data = translated_header_data.astype(data_types_map)
typed_data.dtypes
Out[14]:
datetime                 datetime64[ns]
air_temperature_mean            float32
air_temperature_max             float32
air_temperature_min             float32
dew_point_mean                  float32
dew_point_min                   float32
solar_radiation_mean              int32
vpd_mean                        float32
vpd_min                         float32
air_humidity_mean               float32
air_humidity_max                float32
air_humidity_min                float32
precipitation                   float32
leaf_wetness                      int32
wind_speed_mean                 float32
wind_speed_max                  float32
soil_wetness_mean               float32
soil_temperature_mean           float32
soil_temperature_max            float32
soil_temperature_min            float32
solar_panel                       int32
battery1                          int32
battery2                          int32
eto                              object
dtype: object

Дубликаты¶

Дублирующие друг друга столбцы и строки встечаются в некоторых наборах данных и как минимум приводят к увеличению времени обучения и разработки модели. Необходимо проверить есть ли в наборе данных столбцы и строки с одинаковыми значениями. При их наличии, необходимо оставить только один столбец или строку а остальные дубликаты удалить.

Дубликаты столбцов¶

In [15]:
duplicated_cols = []
for i in range(0, len(typed_data.columns)):
    col_first = typed_data.columns[i]

    for col_second in typed_data.columns[i + 1 :]:
        if typed_data[col_first].equals(typed_data[col_second]):
            duplicated_cols.append(col_second)

print("Дублирующие столбцы:")
duplicated_cols
Дублирующие столбцы:
Out[15]:
[]

В представленном наборе данных дубликатов по столбцам не обнаружено.

Дубликаты строк¶

In [16]:
print("Дублирующие строки:")
len(typed_data) - len(typed_data.drop_duplicates())
Дублирующие строки:
Out[16]:
0

В представленном наборе данных дубликатов по строкам не обнаружено.

Постоянные распределения¶

Некоторые распределения признаков имеют нулевую или очень низкую дисперсию, и часто могут быть неинформативными, а также нести прооблемы для прогнозирования. Лучше найти их заранее и знать о таких переменных. Необходимо исключить столбец с датой, так как он не сможет быть обработан

In [17]:
sel = VarianceThreshold(threshold=0.0)
sel.fit(typed_data.drop(["datetime"], axis=1))
low_variance_cols = (
    len(typed_data.drop(["datetime"], axis=1).columns)
    - sel.get_support().sum()
)
print("Количество столбцов с низкой дисперсией:", low_variance_cols)
Количество столбцов с низкой дисперсией: 0

В представленном наборе данных нет переменных которые бы имели только одно постоянное значение.

Выводы по первичному анализу данных¶

Проведенное первичное исследование набора данных показывает следующие особенности:

  1. Все перемнные набора данных представлены типом данных object, что вероятно означает, что pandas не смог автоматически определить их правильно. Таким образом данные были сконверированы вручную для их дальнейшего анализа
  2. Столбец eto (Эталонная эвапотранспирация) содержит 95% пропусков. Эту особенность данных необходимо исследовать более подробно.
  3. По столбцам и по строкам дубликатов данных не было обнаружено
  4. Постоянные распределения с нулевой дисперсией в наборе данных не были выявлены

Визуальный анализ данных¶

На данном этапе предпринимается попытка визуализировать распределение отдельных переменных, взаимодействия переменных и получить больше информации об исходном наборе данных. Полученные результаты могет быть использованы на этапе разработки признаков.

Исследование средних значений переменных¶

В наборе данных есть паарметры, которые характеризуются тремя значениями: средним, максимальным и минимальным. Будем проводить анализ используя средние заначения переменных, а потом сравним остальные значения переменных со средними, чтобы понять, несут ди они смысловую нагрузку для будущей модели или коррелируют друг с другом.

Период измерений¶

In [18]:
# Год/или годы в которых происходили измерения
measurements_years = pd.to_datetime(typed_data.datetime).dt.year.unique()
# количество дней с записями
start_measurements_day = pd.to_datetime(typed_data.datetime).dt.date.min()
end_measurements_day = pd.to_datetime(typed_data.datetime).dt.date.max()
measurements_days = (end_measurements_day - start_measurements_day).days
print("Период замеров в годах: ", measurements_years)
print("Период замеров в днях: ", measurements_days)
print("День начала замеров: ", start_measurements_day)
print("День окончания замеров: ", end_measurements_day)
Период замеров в годах:  [2020]
Период замеров в днях:  152
День начала замеров:  2020-04-01
День окончания замеров:  2020-08-31

Измерения производились только в 2020 году, в течении 152 дней. Начало замеров стартовало 4 апреля, и закончилось 31 августа. Т.е. в месяцы максимально теплого периода года, с середины весты до конца лета.

In [19]:
# Интервал в течении 2020 года
year_period = date_range("2020-01-01", "2020-12-31")
# Дни года в которые производились земеры
year_day_labels = np.zeros(len(year_period))
# Только даты в которые производились замеры
measurement_days = list(pd.to_datetime(typed_data.datetime).dt.date)

# Находим и помечаем дни в которых были замеры
for year_day in range(len(year_period)):
    target_day = year_period[year_day]
    for measurement_day in measurement_days:
        if measurement_day == target_day:
            year_day_labels[year_day] = 1

# Визуализируем дни замеров
july.heatmap(
    year_period,
    year_day_labels,
    title="Дни в которых производились замеры",
    cmap="github",
    month_grid=True,
    frame_on=True,
);

Пропущенных дней замеров среди периода эксперимента не наблюдается.

Измерения с начала и конца файла¶

In [20]:
# Несколько записей с начала файла
typed_data.head(3).iloc[:, 0:3]
Out[20]:
datetime air_temperature_mean air_temperature_max
1 2020-08-31 19:00:00 19.74 19.82
2 2020-08-31 18:00:00 20.18 20.60
3 2020-08-31 17:00:00 20.57 21.09
In [21]:
# Несколько записей с конца файла
typed_data.tail(3).iloc[:, 0:3]
Out[21]:
datetime air_temperature_mean air_temperature_max
3647 2020-04-01 21:00:00 5.70 5.92
3648 2020-04-01 20:00:00 6.22 7.30
3649 2020-04-01 19:00:00 8.20 8.65

Так как период измерений начинается с 04.01.2020 и заканчивается 31.08.31, то видно что в начале файла расположены более поздние замеры, а в конце файла более ранние. Это стоит учитывать при дальнейшей работе с данными.

Анализ отсуствующих значений¶

На этапе предварительного анализа данных, в столбце eto были обнаружены 95% - отсуствующих значений. Посмотрим на них подробнее.

In [22]:
print(
    "Количество отсуствующих значений в столбце `eto`:",
    len(typed_data[~typed_data["eto"].isna()]),
)
Количество отсуствующих значений в столбце `eto`: 152
In [23]:
# Отображаем столбец времени
typed_data[~typed_data["eto"].isna()]["datetime"].dt.time[:20]
Out[23]:
20     00:00:00
44     00:00:00
68     00:00:00
92     00:00:00
116    00:00:00
140    00:00:00
164    00:00:00
188    00:00:00
212    00:00:00
236    00:00:00
260    00:00:00
284    00:00:00
308    00:00:00
332    00:00:00
356    00:00:00
380    00:00:00
404    00:00:00
428    00:00:00
452    00:00:00
476    00:00:00
Name: datetime, dtype: object

Присуствующих значений - 152, столько же, сколько дней производились замеры в наборе данных. Если отфильтровать по столбцу с датаой и посмотреть на часы, где присутствуют данные, то можно увидеть что данные появляются только в 00:00:00 - один раз в сутки. Видимо это значение определяется 1 раз в сутки и добавляется в набор данных в полночь.

Если посмотреть более подробно, то эвапотранспирация – это комбинация двух процессов: испарения воды с поверхности почвы или влажной поверхности растений и испарение воды растением.Этот показатель расчитывается за день и расчитывается по следующей формуле:

$$ETc = ETr*Kc*Ks$$

где $ETr$ - интенсивность тарансипации, т.е. испарения воды растением (дюймов в день) эталонной культуры (например люцерны), $Kc$ - коэффициент потребления воды культурой, метяющийся в зависимости от фазы развития растения (от 0 до 1), $Ks$ - коэффициент недостатка влаги (обычно от 0 до 1).

Таким образом, понятно что параметр eto, рассчитывается только 1 раз в день, поэтому этот пропуск в данных можно считать намеренным. Таким образом можно заполнить этот столбец значением рассчитанным за прошедшие сутки.

Заполнение пропущенных значений¶

Так как у нас первый замер 04.01.2020 не начинается с полночи, значит мы не можем правильно заполнить отсуствующие в этот день данные, поэтому заполним их средним значением по этому месяцу. Остальные значения можно заполнить установив значение на после расчета по предыдущему дню.

Начинаем циклом проходить с начала файла, т.е. с последенй даты замера, сохранять значения пока встечается NaN, и после обрануженния цифрового значения заполнить предыдущиеNaN найденным значением. Кроме последнего дня, который заполним средним по неделе.

In [24]:
# Проверка на нулевые значения
print(
    "Количество значений 0 в переменной `eto`: ",
    sum(typed_data["eto"].unique() == 0),
)
Количество значений 0 в переменной `eto`:  0

Так как нулевых значений нет в переменной eto, то можно их использовать для временный значений.

In [25]:
# Создаем копию данных
filled_data = typed_data.copy()

eto_variable_values = typed_data["eto"].values
modified_eto_variable_values = np.zeros(shape=(len(eto_variable_values),))

# Индекс остановки
stop_index = 0
for eto_index, eto_value in enumerate(eto_variable_values):
    if not str(eto_value) == "nan":
        modified_eto_variable_values[stop_index : eto_index + 1] = eto_value
        stop_index = eto_index + 1

# Количество оставшихся пустых значений
null_values_count = sum(modified_eto_variable_values == 0)
# Номер прследенй недели
first_week_number = filled_data.datetime.dt.isocalendar().week.min()
# Отбираем записи относящиеся к последенй неделе, кроме последних пустых
# И возьмем среднее значение по ним
mean_first_week_values = filled_data[
    filled_data.datetime.dt.isocalendar().week == first_week_number
][0:-null_values_count]["eto"].mean()

# Заменяем средним значением
modified_eto_variable_values[modified_eto_variable_values == 0] = (
    mean_first_week_values
)
# Заменяем значения в столбце `eto` в наборе данных
filled_data["eto"] = modified_eto_variable_values
filled_data[["datetime", "eto"]]
Out[25]:
datetime eto
1 2020-08-31 19:00:00 1.80
2 2020-08-31 18:00:00 1.80
3 2020-08-31 17:00:00 1.80
4 2020-08-31 16:00:00 1.80
5 2020-08-31 15:00:00 1.80
... ... ...
3645 2020-04-01 23:00:00 2.15
3646 2020-04-01 22:00:00 2.15
3647 2020-04-01 21:00:00 2.15
3648 2020-04-01 20:00:00 2.15
3649 2020-04-01 19:00:00 2.15

3649 rows × 2 columns

Данные были заполнены успешно.

Выбросы в данных¶

Распределения переменных¶

Построим распределения переменных, для определения выбросов

In [26]:
analyzed_cols = [
    "air_temperature_mean",
    "dew_point_mean",
    "solar_radiation_mean",
    "vpd_mean",
    "air_humidity_mean",
    "precipitation",
    "precipitation",
    "leaf_wetness",
    "wind_speed_mean",
    "soil_wetness_mean",
    "soil_temperature_mean",
    "solar_panel",
    "battery1",
    "battery2",
    "eto",
]
analyzed_charts = []

for analyzed_col in analyzed_cols:
    chart = (
        alt.Chart(filled_data)
        .mark_point(size=1)
        .encode(
            x=alt.X("datetime:T", axis=alt.Axis(tickCount=4, grid=True)),
            y=analyzed_col,
        )
        .properties(
            width=200,
            height=200,
            title=["Распределение", "переменной", f"'{analyzed_col}'"],
        )
    )
    analyzed_charts.append(chart)


v1 = alt.vconcat(*(i for i in analyzed_charts[:5]))
v2 = alt.vconcat(*(i for i in analyzed_charts[5:10]))
v3 = alt.vconcat(*(i for i in analyzed_charts[10:]))

(v1 | v2 | v3)
Out[26]:
In [385]:
fig = plt.figure(figsize=(10, 20))
ax = fig.gca()
filled_data[analyzed_cols].hist(ax=ax);

На графиках видно, что некоторые переменные содержат выбросы, в том числе и целевая переменная leaf_wetness, имеет одну точку, которая представлена всего одним значением. Считается, что выбросы это данные которые отличаются от основной массы. Если таких данных больше 15%, то они уже не считаются выбросами - The Effects of Outlier Data on Neural Networks Performance

Расчет границ выбросов для каждой переменной¶

Для того чтобы определить есть ли у переменной выбросы, нуобходимо расчитать квантили распределения а затем межквартильный размах:

$$IQR=75quantile - 25quantile$$

Выбросы будут находиться за пределами нижних и верхних границ:

Верхняя граница = $75quantile + (IQR * 1.5)$

Нижняя граница = $25quantile - (IQR * 1.5)$

In [27]:
cols_outliers = {}

for analyzed_col in analyzed_cols:
    iqr = filled_data[analyzed_col].quantile(0.75) - filled_data[
        analyzed_col
    ].quantile(0.25)
    lower_boundary = filled_data[analyzed_col].quantile(0.25) - (iqr * 1.5)
    upper_boundary = filled_data[analyzed_col].quantile(0.75) + (iqr * 1.5)
    cols_outliers[analyzed_col] = {
        "lower_boundary": lower_boundary,
        "upper_boundary": upper_boundary,
        "iqr": iqr,
    }
In [28]:
pd.DataFrame(cols_outliers).T
Out[28]:
lower_boundary upper_boundary iqr
air_temperature_mean -3.159999 36.279999 9.860000
dew_point_mean -15.849999 39.749999 13.900000
solar_radiation_mean -484.500000 807.500000 323.000000
vpd_mean -1.095000 1.825000 0.730000
air_humidity_mean 17.234993 149.435005 33.050003
precipitation 0.000000 0.000000 0.000000
leaf_wetness -30.000000 50.000000 20.000000
wind_speed_mean -1.950000 4.050000 1.500000
soil_wetness_mean 18.025004 54.944995 9.229998
soil_temperature_mean -3.650001 31.950001 8.900001
solar_panel -14284.500000 23807.500000 9523.000000
battery1 6026.000000 7234.000000 302.000000
battery2 3576.000000 3768.000000 48.000000
eto -0.850000 6.750000 1.900000

К этим данным можно вернуться на этапе разработки признаков, для улучшения модели.

Распределение средней температуры датчиков по дням замеров¶

Так так мы пытаемся прогнозировать влажность листьев которая имеет тенденции к изменениям в течении дня, кажется, что наиболее правильным вариантом является исследование зависимости в дневных средних распределениях данных. Однако снчала необходимо убедиться что в представленных данных нет странностей (как с целевой переменной). Посмотрим как меняется температура воздуха в месте проведения эксперимента в течении 5 месяцев (с весны по конец лета).

Расчет средней температуры по каждому датчику¶

In [152]:
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
temperature_cols = [
    "air_temperature_mean",
    "dew_point_mean",
    "soil_temperature_mean",
]

temp_data_subset = filled_data[["datetime"] + temperature_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
temp_data_subset["datetime"] = temp_data_subset["datetime"].dt.date

# Дата замера
df_data_column = []
# Средняя температура по дням замеров
df_mean_temperature_column = []
# Тип датчика
df_temperature_type_column = []

# Усреднение дневной температуры по средней почасовой по каждому датчику
for temperature_col in temperature_cols:
    for uniq_data in temp_data_subset["datetime"].unique():
        data_group = temp_data_subset[
            temp_data_subset["datetime"] == pd.to_datetime(uniq_data).date()
        ]
        df_data_column.append(str(uniq_data))
        df_mean_temperature_column.append(
            round(data_group[temperature_col].mean(), 2)
        )
        df_temperature_type_column.append(temperature_col)

# Сборка DataFrame
mean_sensor_temperature_df = pd.DataFrame(
    {
        "Дата замера": df_data_column,
        "Средняя температура,°C": df_mean_temperature_column,
        "Датичк": df_temperature_type_column,
    }
)
mean_sensor_temperature_df.head(5)
Out[152]:
Дата замера Средняя температура,°C Датичк
0 2020-08-31 17.299999 air_temperature_mean
1 2020-08-30 17.809999 air_temperature_mean
2 2020-08-29 20.809999 air_temperature_mean
3 2020-08-28 27.090000 air_temperature_mean
4 2020-08-27 24.760000 air_temperature_mean

Отображение сглженных данных по температуре¶

Для того чтобы лучше увидеть тенденции данных, отобразим его сглаженным

In [153]:
# Сглаживание распределений
def smooth_curve(points, factor=0.8):
    smoothed_points = []
    for point in points:
        if smoothed_points:
            previous = smoothed_points[-1]
            smoothed_points.append(previous * factor + point * (1 - factor))
        else:
            smoothed_points.append(point)
    return smoothed_points
In [154]:
# Копирование DataFarame в новый
mean_smoothed_sensor_temperature_df = mean_sensor_temperature_df.copy()
mean_smoothed_sensor_temperature_df["Средняя температура,°C"] = smooth_curve(
    mean_sensor_temperature_df["Средняя температура,°C"]
)

alt.Chart(mean_smoothed_sensor_temperature_df).mark_line().encode(
    x=alt.X("Дата замера:T", axis=alt.Axis(tickCount=4, grid=True)),
    y="Средняя температура,°C",
    color="Датичк",
).properties(
    width=500,
    height=200,
    title=[
        "Тенденция изменения температуры",
        "в течении проведения эксперимента",
    ],
)
Out[154]:

На всем протяжении имерений, видно что температуры со всех датчиков изменяются примерно одинаково. Если наблюдается рост температуры одного датчика,то растет и температура другого и наоборот. Так же видно, что колебания температуры снижаются, дипазон изменений становиться меньше к лету, но в конце лета заметно резкое снижение температуры. Можно сделать вывод что данные похожи на реальные и отображают правильное поведение температуры.

Распределение средней влажности датчиков по дням замеров¶

Расчет средней влажности по каждому датчику¶

Проведем подобный эксперимент с влажностью, в норме она должна иметь похожее поведение

In [155]:
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
wetness_cols = ["air_humidity_mean", "soil_wetness_mean"]

wetness_data_subset = filled_data[["datetime"] + wetness_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
wetness_data_subset["datetime"] = wetness_data_subset["datetime"].dt.date

# Дата замера
df_data_column = []
# Средняя влажность по дням замеров
df_mean_wetness_column = []
# Тип датчика
df_wetness_type_column = []

# Усреднение дневной влажности по средней почасовой по каждому датчику
for wetness_col in wetness_cols:
    for uniq_data in wetness_data_subset["datetime"].unique():
        data_group = wetness_data_subset[
            wetness_data_subset["datetime"]
            == pd.to_datetime(uniq_data).date()
        ]
        df_data_column.append(str(uniq_data))
        df_mean_wetness_column.append(
            round(data_group[wetness_col].mean(), 2)
        )
        df_wetness_type_column.append(wetness_col)

# Сборка DataFrame
mean_sensor_wetness_df = pd.DataFrame(
    {
        "Дата замера": df_data_column,
        "Средняя влажность,%": df_mean_wetness_column,
        "Датичк": df_wetness_type_column,
    }
)
mean_sensor_wetness_df.head(5)
Out[155]:
Дата замера Средняя влажность,% Датичк
0 2020-08-31 91.160004 air_humidity_mean
1 2020-08-30 96.260002 air_humidity_mean
2 2020-08-29 99.970001 air_humidity_mean
3 2020-08-28 82.330002 air_humidity_mean
4 2020-08-27 93.559998 air_humidity_mean

Отображение сглженных данных по влажности¶

In [156]:
# Копирование DataFarame в новый
mean_smoothed_sensor_wetness_df = mean_sensor_wetness_df.copy()
mean_smoothed_sensor_wetness_df["Средняя влажность,%"] = smooth_curve(
    mean_smoothed_sensor_wetness_df["Средняя влажность,%"]
)

alt.Chart(mean_smoothed_sensor_wetness_df).mark_line().encode(
    x=alt.X("Дата замера:T", axis=alt.Axis(tickCount=4, grid=True)),
    y="Средняя влажность,%",
    color="Датичк",
).properties(
    width=500,
    height=200,
    title=[
        "Тенденция изменения влажности воздуха",
        "в течении проведения эксперимента",
    ],
)
Out[156]:

Как и ожидалось, влажность имеет похожее распределение. В целом влажность земли и воздуха также сильно отличаются но следуют похожей тенденции как показатели температуры, т.е. увеличение влажности одного датчика тоже подразумевает увеличения влажности другого, только в разных диапазаонах.

Анализ целевой переменной¶

Целевая переменная, представлена целочисленными значениями. Посмотрим на количество уникальных значений в переменной.

Распределение целевой переменной по количеству дискретных занччений¶

In [157]:
filled_data.leaf_wetness.unique()
Out[157]:
array([ 0,  5, 60, 45, 10, 50, 30, 35, 25, 20, 40, 55, 15, 75, 70],
      dtype=int32)

Переменная содержит всего 15 различных значений, причем все кратные 5. Посмотрим визуально как распределены значения.

In [158]:
value_counts_leaf_wetness_df = (
    filled_data.leaf_wetness.value_counts().reset_index()
)
value_counts_leaf_wetness_df.columns = ["Значение", "Количество"]
alt.Chart(value_counts_leaf_wetness_df).mark_bar().encode(
    x="Значение",
    y="Количество",
).properties(
    title=[
        "Распределение целевой переменной leaf_wetness",
        "по количеству дискретных значений",
    ]
)
Out[158]:

Распределение значений очень неравномерно. Большое количество нулевых значение и очень маленькое количество других.

Количество повторяющихся значений цеелвой перменной¶

In [159]:
filled_data.leaf_wetness.value_counts()
Out[159]:
0     2669
60     690
45      82
5       28
30      25
75      23
40      22
10      20
15      17
35      15
25      15
20      15
50      14
55      13
70       1
Name: leaf_wetness, dtype: int64

Название переменной в исходном наборе данных выглядело как Влажность листа [минимум]', 'time. Изначально можно было предположить, что time означает время, в течении которого лист был влажным. Так как данные представлены с переодичностью в 1 час. Изучив погодную станцию содержащую похожий функционал (https://github.com/polyakovyevgeniy/portfolio_ai/blob/master/Meteobot-Rukovodstvo-RU_ver.1.2.pdf), на странице 15, было указано, что станция собирает даннные каждые 10 минут и отправляет один раз в час. Т.е. ситуация походжа на представленную в данном наборе данных. Т.е. если например взять одно из значений котрое = 15, то получается что 15 минут за этот час лист был влажным. Однако в данных находятся значения больше чем 60. Таким образом данное предположение не соответствует действительности.

Видеофайл размезщенный на Youtube, рассказывает о принципе рабты похожего датчика, где упоминается о том, что он способен измерять уровень влажности. Представленные значения выше 60 могут подходить под уровень влажности в %. Однако остается загадкой, почему этот уровень имеет целочисленные значения, хотя в других переменных уровень влажности представлен как непрерывное значение.

In [37]:
YouTubeVideo("caz1Mspxn-M", width=600)
Out[37]:

Исходя из этого можно сделать вывод о странности представленной целевой переменной. Названия в заголовке переменной в таблице данных противоречит с ее реальниыми значениями. В данном случае допкстим, что представленные значения - это уровень влажности и произведем попытку решить данную задачу.

В случае сложности предсказания представленной целевой переменной попробуем решить задачу как классификацию, где любое значение отличное от нуля будет соответсвовать наличию любой влажности на листе а 0 отсутсвию. К примеру если преодически предсказывать по имеющимся данным отсуствие и наличие влаги, можно делать выводы о предолжительности нахождения влаги на листе, что тоже может оказаться полезным.

Распределение целевой переменной по периоду проведения измерений¶

Расчет средней дневной влажности по периоду измерений¶
In [160]:
# Подвыборка только с datetime и целевой переменной
leaf_wetness_subset = filled_data[["datetime", "leaf_wetness"]].copy()
leaf_wetness_subset["datetime"] = leaf_wetness_subset["datetime"].dt.date
df_data_column = []
df_mean_leaf_wetness_column = []
# Усреднение дневной температуры по средней почасовой по каждому датчику
for uniq_data in leaf_wetness_subset["datetime"].unique():
    data_group = leaf_wetness_subset[
        leaf_wetness_subset["datetime"] == pd.to_datetime(uniq_data).date()
    ]
    df_data_column.append(str(uniq_data))
    df_mean_leaf_wetness_column.append(
        round(data_group["leaf_wetness"].mean(), 2)
    )

# Сборка DataFrame
mean_sensor_leaf_wetness_df = pd.DataFrame(
    {
        "Дата замера": df_data_column,
        "Средняя влажность,%": df_mean_leaf_wetness_column,
    }
)
mean_sensor_leaf_wetness_df.head(5)
Out[160]:
Дата замера Средняя влажность,%
0 2020-08-31 39.25
1 2020-08-30 48.12
2 2020-08-29 47.08
3 2020-08-28 1.25
4 2020-08-27 26.25
In [161]:
# Копирование DataFarame в новый
mean_smothed_sensor_leaf_wetness_df = mean_sensor_leaf_wetness_df.copy()
mean_smothed_sensor_leaf_wetness_df["Средняя влажность,%"] = smooth_curve(
    mean_smothed_sensor_leaf_wetness_df["Средняя влажность,%"]
)

alt.Chart(mean_smothed_sensor_leaf_wetness_df).mark_line().encode(
    x="Дата замера:T",
    y="Средняя влажность,%",
).properties(
    title=[
        "Распределение сглаженной целевой переменной leaf_wetness",
        "по периоду измерений",
    ],
    width=700,
)
Out[161]:

В целом видно что для влажности листьев свойственна повторяющаяся цикличность, налюбюдаются переодические всплески влжности и падения. Есть более выраженные всплески влажности, они наблюдаются в началае лета и ближе к его концу, но их к сожалению мало, и скорее всего нужно будет отбросить как выбросы. Посмотрим более подробно на цикличность среднего месяца, средней недели и среднего дня.

Расчет влажности листьев для среднего месяца, средней недели и среднего дня.¶
In [162]:
# Подвыборка только с datetime и целевой переменной
leaf_wetness_subset = filled_data[["datetime", "leaf_wetness"]].copy()
# Помечаем день месяца, день недели, и час
leaf_wetness_subset["day"] = leaf_wetness_subset.datetime.dt.day
leaf_wetness_subset["dayofweek"] = leaf_wetness_subset.datetime.dt.dayofweek
leaf_wetness_subset["hour"] = leaf_wetness_subset.datetime.dt.hour
# Максимальное и минимальное количество дней в месяце за весь период измерений
max_month_length = leaf_wetness_subset.datetime.dt.day.max()
min_month_length = leaf_wetness_subset.datetime.dt.day.min()
# Максимальное и минимальное количество дней в неделе за весь период измерений
max_week_length = leaf_wetness_subset.datetime.dt.dayofweek.max()
min_week_length = leaf_wetness_subset.datetime.dt.dayofweek.min()
# Максимальное и минимальное количество часов в дне за весь период измерений
max_hour_length = leaf_wetness_subset.datetime.dt.hour.max()
min_hour_length = leaf_wetness_subset.datetime.dt.hour.min()

# DataFrame по каждому из периодов
mean_leaf_wetness_key_periods_dfs = []
# Усреднение дневной температуры по средней почасовой по каждому датчику
for period, max_value, min_value, title in zip(
    ["day", "dayofweek", "hour"],
    [max_month_length, max_week_length, max_hour_length],
    [min_month_length, min_week_length, min_hour_length],
    ["День месяца", "День недели", "Час дня"],
):
    df_data_column = []
    df_mean_wetness_column = []
    for period_value in range(min_value, max_value + 1):
        mean_leaf_wetness = round(
            leaf_wetness_subset[leaf_wetness_subset[period] == period_value][
                "leaf_wetness"
            ].mean(),
            2,
        )
        df_data_column.append(period_value)
        df_mean_wetness_column.append(mean_leaf_wetness)

    mean_leaf_wetness_key_periods_dfs.append(
        pd.DataFrame(
            {
                title: df_data_column,
                "Средняя влажность,%": df_mean_wetness_column,
            }
        )
    )

for key_period_df in mean_leaf_wetness_key_periods_dfs:
    display(key_period_df.sample(2))
    print("\n")
День месяца Средняя влажность,%
30 31 16.18
29 30 13.54

День недели Средняя влажность,%
4 4 11.05
2 2 18.99

Час дня Средняя влажность,%
15 15 10.53
5 5 22.47

Графики средних по казателей влажности листьев¶
In [163]:
# Графики средних по казателей влажности листьев
# по периодам ['День месяца', 'День недели', 'Час дня']
mean_leaf_wetness_key_period_charts = []
mean_leaf_column_name = "Средняя влажность,%"
for mean_leaf_wetness_key_period_df, period_value_col_name in zip(
    mean_leaf_wetness_key_periods_dfs,
    [df.columns[0] for df in mean_leaf_wetness_key_periods_dfs],
):
    # Копирование DataFarame в новый
    mean_smoothed_leaf_wetness_key_period_df = (
        mean_leaf_wetness_key_period_df.copy()
    )

    mean_smoothed_leaf_wetness_key_period_df[mean_leaf_column_name] = (
        smooth_curve(
            mean_smoothed_leaf_wetness_key_period_df[mean_leaf_column_name]
        )
    )

    chart_base = alt.Chart(mean_leaf_wetness_key_period_df).encode(
        x=period_value_col_name,
        y=mean_leaf_column_name,
    )
    chart_bar = chart_base.mark_bar().encode(
        x=period_value_col_name,
        y=mean_leaf_column_name,
    )
    chart_line = (
        alt.Chart(mean_smoothed_leaf_wetness_key_period_df)
        .mark_line(color="red")
        .encode(
            x=period_value_col_name,
            y=mean_leaf_column_name,
        )
    )
    compound_chart = (chart_bar + chart_line).properties(
        title=[
            "Распределение целевой переменной leaf_wetness",
            f"по периоду '{period_value_col_name}'",
        ],
        width=700,
        height=100,
    )
    mean_leaf_wetness_key_period_charts.append(compound_chart)
alt.vconcat(*mean_leaf_wetness_key_period_charts)
Out[163]:

В течении месяца всплесков происходит большое количество, если смотреть в недельном выражении то в среднем во вторник влажность выше чем в другие дни. Если рассматривать тенденции в дневном выражении то всреднем с 3 часов ночи до 8 утра наблюдается сама высокая влажность листьев. Эти все особенности можно будет использовать на этапе разработки признаков, если качество модели будет не достаточным для ее использования в производственной среде.

Распределение других переменных¶

Рассмотрим распределение средних показателей переменных wind_speed_mean, precipitation, solar_radiation_mean, vpd_mean

Расчет средних показателей других переменных по периоду измерений¶

In [164]:
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
other_cols = [
    "wind_speed_mean",
    "precipitation",
    "solar_radiation_mean",
    "vpd_mean",
]

other_data_subset = filled_data[["datetime"] + other_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
other_data_subset["datetime"] = other_data_subset["datetime"].dt.date

mean_other_dfs = []
mean_other_titles = [
    "Скорость ветра,m/s",
    "Осадки,mm",
    "Солнечная радиация,W/m2",
    "VPD,kPa",
]
# Усреднение дневных показателей по каждому датчику
for other_col, title in zip(
    other_cols,
    mean_other_titles,
):
    # Дата замера
    df_data_column = []
    # Средняя влажность по дням замеров
    df_mean_other_column = []
    for uniq_data in other_data_subset["datetime"].unique():
        data_group = other_data_subset[
            other_data_subset["datetime"] == pd.to_datetime(uniq_data).date()
        ]
        df_data_column.append(str(uniq_data))
        df_mean_other_column.append(round(data_group[other_col].mean(), 2))
    mean_other_dfs.append(
        pd.DataFrame(
            {"Дата замера": df_data_column, title: df_mean_other_column}
        )
    )

for mean_other_df in mean_other_dfs:
    display(mean_other_df.sample(2))
    print("\n")
Дата замера Скорость ветра,m/s
15 2020-08-16 0.53
40 2020-07-22 0.38

Дата замера Осадки,mm
29 2020-08-02 0.2
18 2020-08-13 0.0

Дата замера Солнечная радиация,W/m2
108 2020-05-15 220.88
148 2020-04-05 85.00

Дата замера VPD,kPa
152 2020-04-01 0.52
130 2020-04-23 0.19

Графики средних других переменных¶

In [165]:
mean_other_charts = []
for mean_other_df, title in zip(mean_other_dfs, mean_other_titles):
    # Копирование DataFarame в новый
    mean_smooth_other_df = mean_other_df.copy()
    mean_smooth_other_df[title] = smooth_curve(mean_smooth_other_df[title])
    chart = (
        alt.Chart(mean_smooth_other_df)
        .mark_line()
        .encode(
            x="Дата замера:T",
            y=title,
        )
        .properties(
            title=[
                f"Распределение сглаженной переменной '{title}'",
                "по периоду измерений",
            ],
            width=700,
            height=200,
        )
    )
    mean_other_charts.append(chart)
alt.vconcat(*mean_other_charts)
Out[165]:

Показатели Скорость ветра, Осадки, Солнечная радиация, VPD тоже имеют тенденции к переодическим изменениям, можно изучить их более подробно во взаимодействии с целевой переменной

Сравние различных средних дневных показателей¶

Сравним все исследованные показатели вместе по измененям в течении дня

In [166]:
cols_for_analysis = [
    "leaf_wetness",
    "air_temperature_mean",
    "soil_temperature_mean",
    "dew_point_mean",
    "air_humidity_mean",
    "soil_wetness_mean",
    "vpd_mean",
    "precipitation",
    "wind_speed_mean",
    "solar_radiation_mean",
]
cols_titles = [
    "Влажность листа,%",
    "Температура воздуха,°C",
    "Температура земли,°C",
    "Точка росы,°C",
    "Влажность воздуха,%",
    "Влажность земли,%",
    "VPD,kPa",
    "Осадки,mm",
    "Скорость ветра,m/s",
    "Солдечная радиация,W/m2",
]

charts_arr = []

data_subset = filled_data[["datetime"] + cols_for_analysis].copy()
# Приведение даты к типу datetime
data_subset["datetime"] = pd.to_datetime(data_subset.datetime.copy()).copy()
data_subset["dayofyear"] = data_subset["datetime"].dt.dayofyear.copy()
data_subset["hour"] = data_subset["datetime"].dt.hour

for col in cols_for_analysis:
    data_subset[col] = smooth_curve(data_subset[col].values)

select_dayofyear = alt.selection_single(
    name="select",
    fields=["dayofyear"],
    init={"dayofyear": 145},
    bind=alt.binding_range(min=92, max=244, step=1),
)

for i, col in enumerate(cols_for_analysis):
    width = 310
    height = 100
    color = "#3287a8"
    if col == "leaf_wetness":
        # width = 680
        # height = 100
        color = "#a83240"
    chart = (
        alt.Chart(data_subset)
        .mark_line()
        .encode(
            x=alt.X("hour:N", axis=alt.Axis(grid=True), title="Час суток"),
            y=alt.Y(col, title=cols_titles[i].split(" ")),
            color=alt.value(color),
            strokeWidth=alt.value(3),
        )
        .properties(width=width, height=height)
    )

    charts_arr.append(chart)


v1 = alt.vconcat(*(i for i in charts_arr[:5]))
v2 = alt.vconcat(*(i for i in charts_arr[5:]))

(v1 | v2).add_selection(select_dayofyear).transform_filter(select_dayofyear)
Out[166]:

Средний день по всем датчикам¶

Сравнение всех показателей по среднему дню

In [167]:
cols_for_analysis = [
    "leaf_wetness",
    "air_temperature_mean",
    "soil_temperature_mean",
    "dew_point_mean",
    "air_humidity_mean",
    "soil_wetness_mean",
    "vpd_mean",
    "precipitation",
    "wind_speed_mean",
    "solar_radiation_mean",
]
cols_titles = [
    "Влажность листа,%",
    "Температура воздуха,°C",
    "Температура земли,°C",
    "Точка росы,°C",
    "Влажность воздуха,%",
    "Влажность земли,%",
    "VPD,kPa",
    "Осадки,mm",
    "Скорость ветра,m/s",
    "Солдечная радиация,W/m2",
]

charts_arr = []

data_subset = filled_data[["datetime"] + cols_for_analysis].copy()
# Приведение даты к типу datetime
data_subset["datetime"] = pd.to_datetime(data_subset.datetime.copy()).copy()
data_subset["hour"] = data_subset["datetime"].dt.hour
max_hour = data_subset["hour"].max()
min_hour = data_subset["hour"].min()

mean_hour_dfs = []

# Усреднение дневных показателей по каждому датчику
for col_for_analysis, title in zip(
    cols_for_analysis,
    cols_titles,
):
    # Дата замера
    df_data_column = []
    # Средняя влажность по дням замеров
    df_mean_hour_column = []
    for hour in range(min_hour, max_hour + 1):
        data_group = data_subset[data_subset["hour"] == hour]
        df_data_column.append(str(hour))
        df_mean_hour_column.append(
            round(data_group[col_for_analysis].mean(), 2)
        )
    mean_hour_dfs.append(
        pd.DataFrame(
            {"Час суток": df_data_column, title: df_mean_hour_column}
        )
    )


for i, col in enumerate(cols_titles):
    color = "#a83240"
    if col == "Влажность листа,%":
        color = "green"
    chart = (
        alt.Chart(mean_hour_dfs[i])
        .mark_area()
        .encode(
            x=alt.X("Час суток:Q", axis=alt.Axis(grid=True)),
            y=alt.Y(col, title=cols_titles[i].split(" ")),
            color=alt.value(color),
            strokeWidth=alt.value(2),
        )
        .properties(width=310, height=150)
    )
    charts_arr.append(chart)


v1 = alt.vconcat(*(i for i in charts_arr[:5]))
v2 = alt.vconcat(*(i for i in charts_arr[5:]))

(v1 | v2)
Out[167]:

Усреднив все часовые показатели по всем наблюдениям можно увидеть как некоторые показатели имеют тенденцию к повторению значений других переменных. Например заметно некоторые похожие тенденции во влажности воздуха и влажности листьев и обратную тенденцию между влажностью листьев и VPD. Температура воздуха и точка росы имеют похожее поведение. Увеличение солнечной радиации, скорости ветра и повышение температуры воздуха приводит к уменьшению влажности листьев.

Распределение максимальных и минимальных занчений в некоторых переменных¶

Некоторые переменные имеют дополнительные данные (кроме среднего) в виде минимальных и максимальных значений. Скорее всего эти данные будут повторять средние значения, однако необходимо убедиться, что это так.

In [168]:
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
mean_max_min_cols = [
    ["air_temperature_mean", "air_temperature_max", "air_temperature_min"],
    ["dew_point_mean", "dew_point_min"],
    ["vpd_mean", "vpd_min"],
    ["air_humidity_mean", "air_humidity_max", "air_humidity_min"],
    ["wind_speed_mean", "wind_speed_max"],
    ["soil_temperature_mean", "soil_temperature_max", "soil_temperature_min"],
]

mean_max_min_titles = [
    "Температура воздуха,°C",
    "Точка росы,°C",
    "VPD,kPa",
    "Влажность воздуха,%",
    "Скорость ветра,m/s",
    "Температура почвы,°C",
]

mean_max_min_subset = filled_data[
    ["datetime"] + [y for x in mean_max_min_cols for y in x]
].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
mean_max_min_subset["datetime"] = mean_max_min_subset["datetime"].dt.date


# Усреднение дневной влажности по средней почасовой по каждому датчику
mean_max_min_sensor_arr = []

for sensor_subset, title in zip(mean_max_min_cols, mean_max_min_titles):
    # Дата замера
    df_data_column = []
    # Средняя влажность по дням замеров
    df_mean_max_min_column = []
    # Тип датчика
    df_mean_max_min_type_column = []
    for sensor in sensor_subset:
        for uniq_data in mean_max_min_subset["datetime"].unique():
            data_group = mean_max_min_subset[
                mean_max_min_subset["datetime"]
                == pd.to_datetime(uniq_data).date()
            ]
            df_data_column.append(str(uniq_data))
            df_mean_max_min_column.append(round(data_group[sensor].mean(), 2))
            df_mean_max_min_type_column.append(sensor)

    mean_max_min_sensor_arr.append(
        pd.DataFrame(
            {
                "Дата замера": df_data_column,
                title: df_mean_max_min_column,
                "Датчик": df_mean_max_min_type_column,
            }
        )
    )

mean_max_min_sensor_chart_arr = []

for mean_max_min_sensor_df in mean_max_min_sensor_arr:
    mean_max_min_smooth_sensor_df = mean_max_min_sensor_df.copy()
    title = mean_max_min_smooth_sensor_df.columns[1]
    mean_max_min_smooth_sensor_df[title] = smooth_curve(
        mean_max_min_smooth_sensor_df[title]
    )
    chart = (
        alt.Chart(mean_max_min_smooth_sensor_df)
        .mark_line()
        .encode(x="Дата замера:T", y=title, color="Датчик")
        .properties(width=300, height=150)
    )
    mean_max_min_sensor_chart_arr.append(chart)

v1 = alt.vconcat(*(i for i in mean_max_min_sensor_chart_arr[:3]))
v2 = alt.vconcat(*(i for i in mean_max_min_sensor_chart_arr[3:]))

(v1 | v2)
Out[168]:

Как и ожидалось максимальные и минимальные значения аналогичны средним, поэтому врядли могут принести большую пользу для улучшению модели.

Распределение переменных с данными элементов питания¶

In [169]:
# Подвыборака только с нужными столбцами (дата и параметры с температурами)
battery_cols = ["solar_panel", "battery1", "battery2"]
# Копируем выборку
battery_data_subset = filled_data[["datetime"] + battery_cols].copy()
# Приведение даты к типу datetime.date для поиска только по дням (исключая часы)
battery_data_subset["datetime"] = battery_data_subset["datetime"].dt.date

# Дата замера
df_data_column = []
# Средняя влажность по дням замеров
df_mean_battery_charge_column = []
# Тип батареи
df_battery_charge_type_column = []

# Усреднение зарада батареи по дням по каждому датчику
for battery_col in battery_cols:
    for uniq_data in battery_data_subset["datetime"].unique():
        data_group = battery_data_subset[
            battery_data_subset["datetime"]
            == pd.to_datetime(uniq_data).date()
        ]
        df_data_column.append(str(uniq_data))
        df_mean_battery_charge_column.append(
            int(data_group[battery_col].mean())
        )
        df_battery_charge_type_column.append(battery_col)

# Сборка DataFrame
mean_battery_charge_df = pd.DataFrame(
    {
        "Дата замера": df_data_column,
        "Средний заряд,mV": df_mean_battery_charge_column,
        "Датичк": df_battery_charge_type_column,
    }
)
mean_battery_charge_df.head(5)
Out[169]:
Дата замера Средний заряд,mV Датичк
0 2020-08-31 5264 solar_panel
1 2020-08-30 3909 solar_panel
2 2020-08-29 3331 solar_panel
3 2020-08-28 4473 solar_panel
4 2020-08-27 4324 solar_panel
In [170]:
# Копирование DataFarame в новый
mean_smoothed_battery_charge_df = mean_battery_charge_df.copy()

alt.Chart(mean_battery_charge_df).mark_bar().encode(
    x=alt.X("Дата замера:T", axis=alt.Axis(tickCount=4, grid=True)),
    y="Средний заряд,mV",
    color="Датичк",
).properties(
    width=700,
    height=200,
    title=[
        "Заряд батарей и тока выдаваемого солнчной панелью",
        "в течении проведения эксперимента",
    ],
)
Out[170]:

Заряд батарей всегда находиться в стабильном состоянии. Напряжение выдаваемое солнечной панелью меняется, так как это зависит от солнечного света. На графике преставлены средние значения заряда. Чтобы убедиться выдает ли солнечная батарея нулевое напряжение в ночное время, можно посмотреть исходные данные.

In [171]:
filled_data[filled_data.solar_panel == 0][["datetime", "solar_panel"]][:5]
Out[171]:
datetime solar_panel
14 2020-08-31 06:00:00 0
15 2020-08-31 05:00:00 0
16 2020-08-31 04:00:00 0
17 2020-08-31 03:00:00 0
18 2020-08-31 02:00:00 0
In [172]:
filled_data[filled_data.solar_panel != 0][["datetime", "solar_panel"]][:5]
Out[172]:
datetime solar_panel
1 2020-08-31 19:00:00 9058
2 2020-08-31 18:00:00 9298
3 2020-08-31 17:00:00 8349
4 2020-08-31 16:00:00 9125
5 2020-08-31 15:00:00 9605

Видно, что в дневное время выдаваемое напряжение не нулевое, тогда как ночью оно равно нулю, что говорит о том, что скорее всего замеры были сделаны на улице (не в теплице).

В целом, значения заряда батарей стабильны и нормальны, и врядли могут как-то влиять на процесс образования влаги на поверхности листа, скорее всего их нужно удалить за ненадобностью.

Удаление переменных связанных с элементами питания¶

In [173]:
deleted_battery_data = filled_data.drop(
    ["solar_panel", "battery1", "battery2"], axis=1
)
In [174]:
print("Количество столбцов с элементами питания: ", len(filled_data.columns))
print(
    "Количество столбцов без элементов питания: ",
    len(deleted_battery_data.columns),
)
Количество столбцов с элементами питания:  24
Количество столбцов без элементов питания:  21

Критерий нормальности Шапиро-Уилка¶

... для проверки на нормальность для линейных моделей...

Корреляции признаков с усреднением по часам¶

Посмотрим корреляции переменных между собой

In [176]:
corr = (
    deleted_battery_data[
        [
            "leaf_wetness",
            "air_temperature_mean",
            "soil_temperature_mean",
            "dew_point_mean",
            "air_humidity_mean",
            "soil_wetness_mean",
            "vpd_mean",
            "precipitation",
            "wind_speed_mean",
            "solar_radiation_mean",
        ]
    ]
).corr(numeric_only=True, method="spearman")
mask = np.zeros_like(corr, dtype=np.bool_)
mask[np.triu_indices_from(mask)] = (
    True  # Верхнему треугольнику присваиваем True
)
plt.figure(figsize=(5, 5))
sns.set(font_scale=0.8)
sns.heatmap(
    corr,
    mask=mask,
    vmax=1,
    center=0,
    annot=True,
    fmt=".1f",
    square=True,
    linewidths=0.5,
    cbar_kws={"shrink": 1},
);

Определенную корреляцию с целевой переменной имеет vpd и влажность воздуха. Это говорит о том, что скорее всего линейные модели не принесут высокого качества моделирования. На этапе базового решения попробуем построить линейную модель, чтобы убедиться в этом предположении.

Выводы по визуальному анализу данных¶

In [ ]:
...
Out[ ]:
Ellipsis

Моделирование¶

Исходя из проведенного исследования, можно сделать вывод, что целевая переменная не имеет линейной зависимости от параметров, поэтому использование линейной регресси врядли принесет хорошие результаты. Так как цель данной работы определить возможность получения прогнозной модели по имеющимся данным, и учитывая сложившуюся ситуацию будем исползовать разнородные модели чтобы оценить какой тип наиболее приемлем в данной задаче и в данных условиях. Будем обучать используя: метод опопрных векторов, деревья решений, случайный лес, градиентный бустинг деревьев решений и нейронные сети.

Метрики качества¶

Для данной задачи вполне подойдет стандартный коэфициент детерминации $R^2$, однако можно использовать дополнительно среднюю абсолютную (mse) ошибку чтобы понять на сколько сильно в обсалютных числах наша модель будет ошибаться.

Создание обучающего набора¶

Набор даннных содердит 3649 объектов, что не является большим числом но и не сильно мало. Попробуем сначала обучить модель без использования кросс-валидации, и будем надеяться, что данных хватит для обобщения модели. Если модель будет недообучаться тогда используем кросс-валидацию вместо тестового набора данных для экономии ценных объектов. Но в случае с нейронными сетями все же нужно будет использовать проверочный набор данных, иначе сложно будет найти баланс между недообученной и переобученной моделью.

Обучающий набор для задачи регрессии¶

In [318]:
# Удаление колонки "datetime"
modeling_data = deleted_battery_data.drop(["datetime"], axis=1).copy()

# Разделение на тренировочный и тестовый набор данных
X_reg_train, X_reg_test, y_reg_train, y_reg_test = train_test_split(
    modeling_data.drop(["leaf_wetness"], axis=1),
    modeling_data["leaf_wetness"],
    random_state=RANDOM_STATE,
    shuffle=True,
    test_size=0.2,
)
print("Размеры тренировочной выборки:")
display(X_reg_train.shape)
print("Размеры тестовой выборки:")
display(X_reg_test.shape)
Размеры тренировочной выборки:
(2919, 19)
Размеры тестовой выборки:
(730, 19)

Обучающий набор для задачи классификации¶

In [365]:
modeling_data_classification = modeling_data.copy()
modeling_data_classification.loc[
    modeling_data_classification["leaf_wetness"] > 1, "leaf_wetness"
] = 1

# Разделение на тренировочный и тестовый набор данных
X_cls_train, X_cls_test, y_cls_train, y_cls_test = train_test_split(
    modeling_data_classification.drop(["leaf_wetness"], axis=1),
    modeling_data_classification["leaf_wetness"],
    random_state=RANDOM_STATE,
    shuffle=True,
    test_size=0.2,
)

print("Количество классов:")
display(modeling_data_classification["leaf_wetness"].value_counts())
print("Размеры тренировочной выборки:")
display(X_cls_train.shape)
print("Размеры тестовой выборки:")
display(X_test.shape)
Количество классов:
0    2669
1     980
Name: leaf_wetness, dtype: int64
Размеры тренировочной выборки:
(2919, 19)
Размеры тестовой выборки:
(913, 19)

Базовое решение¶

Обучим без предобработок и настроек классические ML-модели в качестве базового решения.

Регрессия¶

Линейная регрессия¶

Обучим линейную регрессию. Для линейной регрессии нужно стандартизировать или нормализовать данные. Что лучше продходит для данных представленных распределений данных, можно узнать с помощью тестирования. На следующем этапе можно будет определиться с этим.

In [349]:
baseline_reg_scaler = StandardScaler()
X_reg_train_scaled = baseline_reg_scaler.fit_transform(X_reg_train)
X_reg_test_scaled = baseline_reg_scaler.transform(X_reg_test)

baseline_linreg_reg_model = LinearRegression().fit(
    X_reg_train_scaled, y_reg_train
)
display(
    baseline_linreg_reg_model.score(X_reg_train_scaled, y_reg_train.values)
)
display(baseline_linreg_reg_model.score(X_reg_test_scaled, y_reg_test.values))
0.3999891497607484
0.3835676710677669

Сразу видно плохое качество линейной модели, так как действительно исходя из матрицы корреляций было понятно, что только две переменные имели небольшую корреляцию с целевой перменной.

Деревья решений¶

Для моделей на основе деревьев не нужно применять масштабирование данных

In [345]:
baseline_dtree_reg_model = DecisionTreeRegressor()
baseline_dtree_reg_model.fit(X_reg_train, y_reg_train)
display(baseline_dtree_reg_model.score(X_reg_train, y_reg_train))
display(baseline_dtree_reg_model.score(X_reg_test, y_reg_test))
1.0
0.5185649046709446

Деревья решений сразу преобучаются, поэтому обязательно для них нужно производить настройку гиперпараметров

Случайный лес¶

In [346]:
baseline_rf_reg_model = RandomForestRegressor()
baseline_rf_reg_model.fit(X_reg_train, y_reg_train)
display(baseline_rf_reg_model.score(X_reg_train, y_reg_train))
display(baseline_rf_reg_model.score(X_reg_test, y_reg_test))
0.9536238152288552
0.729635378925314

Случайный лес тоже сильно переобучился, но уже показывает более качественные результаты

Градиентный бустинг деревьев решений¶

In [347]:
baseline_xgb_reg_model = xgboost.XGBRegressor()
baseline_xgb_reg_model.fit(X_reg_train, y_reg_train)
display(baseline_xgb_reg_model.score(X_reg_train, y_reg_train))
display(baseline_xgb_reg_model.score(X_reg_test, y_reg_test))
0.9923912318107077
0.758851654614346

Среди выбранных моделей, градиентный бустинг сразу показывает наиболее высокие результаты.

Классификация¶

Линейная модель¶

In [371]:
baseline_cls_scaler = StandardScaler()
X_cls_train_scaled = baseline_cls_scaler.fit_transform(X_cls_train)
X_cls_test_scaled = baseline_cls_scaler.transform(X_cls_test)

baseline_logreg_clc_model = LogisticRegression().fit(
    X_cls_train_scaled, y_cls_train
)

display(
    roc_auc_score(
        baseline_logreg_clc_model.predict(X_cls_train_scaled),
        y_cls_train.values,
    )
)
display(
    roc_auc_score(
        baseline_logreg_clc_model.predict(X_cls_test_scaled),
        y_cls_test.values,
    )
)
0.8211316721767364
0.8211992588123761

Деревья решений¶

In [372]:
baseline_dtree_cls_model = DecisionTreeClassifier().fit(
    X_cls_train_scaled, y_cls_train
)

display(
    roc_auc_score(
        baseline_dtree_cls_model.predict(X_cls_train_scaled),
        y_cls_train.values,
    )
)
display(
    roc_auc_score(
        baseline_dtree_cls_model.predict(X_cls_test_scaled),
        y_cls_test.values,
    )
)
1.0
0.8431286549707603

Случайный лес¶

In [373]:
baseline_rf_cls_model = RandomForestClassifier().fit(
    X_cls_train_scaled, y_cls_train
)

display(
    roc_auc_score(
        baseline_rf_cls_model.predict(X_cls_train_scaled),
        y_cls_train.values,
    )
)
display(
    roc_auc_score(
        baseline_rf_cls_model.predict(X_cls_test_scaled),
        y_cls_test.values,
    )
)
1.0
0.9022908622908623

Градиентный бустинг деревьев решений¶

In [376]:
baseline_xgb_cls_model = xgboost.XGBClassifier().fit(
    X_cls_train_scaled, y_cls_train
)

display(
    roc_auc_score(
        baseline_xgb_cls_model.predict(X_cls_train_scaled),
        y_cls_train.values,
    )
)
display(
    roc_auc_score(
        baseline_xgb_cls_model.predict(X_cls_test_scaled),
        y_cls_test.values,
    )
)
1.0
0.8956143494186973

Список дальнейших исследований¶

  1. Базовое решение показало, что наиболее приемлемыми вариантами для моделирования являеются модели случайного леса и градиентного бустинга деревьев решений. Эти модели и будут дальше использоваться как основные.

  2. Нейронные сети могут оказаться как приемлемым вариантом так и не приемлемым, данных может оказаться слишком мало и нейронная сеть будет или переобучаться или вовсе недообучаться в зависимости от обобщающей способности данных.

  3. Для нейронных сетей определить приемлемый вариант масштабирования данных. Стандартизация или нормализация. Для этого можно обучить линейную модель, используя нормализованные данные и стандартизованные. Где качество модели будет лучше тот тип масштабирования и выбрать. Для моделей основанных на деревьях масштабирование признаков не требуется.

  4. Необходимо снчала выполнить различные манипуляции над данными. После каждой манипуляции необходимо переобучать выбранный список моделей сначала на дефолтных данных, и следить за ростом или увеличением значений метрик.

  • 4.1. Удалить переменные с максимальными и минимальными значениями параметров, оставив только средние, так как они повторяют поведение средних. Проверить качество модели.
  • 4.2. Удалить переменные влажности и температуры почвы и проверить качество
  • 4.3. Удалить выбросы в данных основываясь на приведенном анализе.
  • 4.4. Сделать отбор признаков используя деревья решений (посмотреть важность признаков для модели, оставить только важные).
  • 4.5. Попытаться из удаленных ранее переменных сделать новые признаки и добавить эти признаки в модель. Сделать над ними PCA преобразования для получения более низкой размерности.
  1. Настроить гиперпараметры моделей используя инструмент Optuna
  2. Разработка признаков:
  • 6.1. Так как обнаружились дневные тренды в данных, можно извлечь параметр часа и добавить как переменную в модель, что может улучшить качество модели.
  • 6.2. Так как обнаружились тренды по росту средней дневной температуры и влажности к концу лета, то параметры месяца могут дать прирост, теоретически какое-то улучшение качества могут дать параметры недели. (Т.е. извлечь номер месяца и номер недели.) Это может ограничить использование модели так как придется передавать нужный месяц, но может помочь лучше прогнозировать в существующем диапазоне.
  1. Для использования модели написать Docker-обертку и в зависимости от типа использовать Seldon MLServer или TensorFlow Serving для Инференса.

Выводы:

  1. Модель будет ограничена в диапазонах предсказания. Так высокие значения влажности листьев в исходных данных являются выбросами, то модель не сможет предсказывать влажность выше 60 градусов. Данное правило относиться к моделям основанных на деревьях, так как они могут предсказывать средние значения из обучающего набора и не могут прогнозировать в будущее. Если нейронная сеть будет лучше, тогда данный прогноз возможен и модель будет менее ограничена.
  2. Модель ограничена по времени использования, если мы введем параметры месяца то это может иметь еще более выраженный эффект.
  3. Для улчшения прогнозирования модели желательно собрать данных за более длительне периоды и использовать в качестве целевой переменной не дискретные а непрерывные значения.

Полезная информация для агроиндстрии:

  1. По анализу данных было установлено, что измерения проводились в условиях открытой местности. Так как по "среднему дню" видно, что увеличение солнечной радиации, скорости ветра и повышение температуры воздуха приводит к уменьшению влажности листьев, то можно рекомендовать использовать для культур которые чувствительны к влажности выращивать их в тепличных условиях, где возможно устанавливать контроль над такими параметрами.